RESTful Hypermedia API サーバサイド編 – garage
モバイルアプリサービス部の五十嵐です。
前回の記事ではハイパーメディアAPIの概要について説明しました。これから何回かにわたって、Ruby on RailsでハイパーメディアAPIを実装するためのgemを紹介したいと思います。今回はcookpadさんが提供されているgarage
というgemを試してみます。
Garageとは
Garageは、Ruby on RailsにRESTful hypermedia APIを追加するgemです。
Rails framework to add RESTful hypermedia API to your application.
GarageはRailsネイティブのRESTfulなルーティングを使い、シンプルなHypermediaフレンドリーなRESTful APIを提供します。中略。またDoorkeeperなどを使用したOauth2認証や、リソースベースのアクセスコントロールを提供します。
Garage provides a simple, Hypermedia friendly RESTful API to your Rails application using its native RESTful routes. Garage provides a descriptive way to serve your ActiveRecord models, as well as plain old Ruby objects as JSON-based resources.
Garage supports OAuth 2 authorizations via Doorkeeper (more extensions to come), and provides resource-based access controls.
(公式リポジトリのREADME.mdより)
サンプルプログラム
クックパッド開発者ブログで紹介されている手順を参考にしつつ、Hypermediaフレンドリーな機能も試してみたいと思います。実装する内容はブログの通りですが、以下に抜粋を記載します。
・アプリケーションが提供するリソースはログインユーザーである user と投稿された投稿である post の2つ。 ・user について以下の操作を提供します ・ユーザーの一覧の表示 GET /v1/users ・それぞれのユーザーの情報の表示 GET /v1/users/:user_id ・自身の情報の更新 PUT /v1/users/:user_id ・post については以下の操作を提供します。 ・新規記事の作成 POST /v1/posts ・アプリケーション全体の記事の一覧の表示 GET /v1/posts ・あるユーザーの投稿した記事一覧の表示 GET /v1/users/:user_id/posts ・それぞれの記事の情報の表示 GET /v1/posts/:post_id ・自身の投稿した記事の更新 PUT /v1/posts/:post_id ・投稿した記事の削除 DELETE /v1/posts/:post_id ・user の作成や削除については実装しません。
また、本記事の目的はハイパーメディアAPIの実装なので、認証やテストなど本題と外れるところは省略していきます。今回のサンプルコードはGitHubのリポジトリにありますので参考にしてください。サンプルコードには簡単なテストも書いています。
動作環境
- Ruby (2.2.2)
- rails (4.2.1)
- garage (1.5.2)
Rails new
まずアプリケーションを作成します。
rails new blog --skip-bundle --skip-test-unit -q && cd blog
次にGemfileに必要なgemを設定していきます。何点か注意がありますが、まずgarage
はGitHubのcookpad/garage
を参照してください。rubygemsに登録されているgarage
は全く別物です。また、今回は省略しますが認証にdoorkeeperを使用される場合、最新のgarage
ではdoorkeeper
はgarage
に含まれませんので、別途garage-doorkeeper
を追加する必要があります。responders
もgarage
が使うので設定しておきます。
# Gemfile gem 'garage', github: 'cookpad/garage' gem 'responders', '~> 2.0'
設定したらbundle installを行います。
bundle install
Configuration and authentication/authorization
次にgarage
の設定を行います。Garage.configure
にはgarage本体の設定をします。Garage::TokenScope.configure
にはアクセスコントロールの設定を行います。Garage.configuration.strategy
には認証の方法を設定します。今回は認証は行わないので、Garage::Strategy::NoAuthentication
を設定します。
# config/initializers/garage.rb Garage.configure {} Garage::TokenScope.configure {} Garage.configuration.strategy = Garage::Strategy::NoAuthentication
コントローラの作成
applicationコントローラにはGarage::ControllerHelper
を追加します。これはコントローラ共通で使用するフィルタとメソッドが提供されます。今回は使用しませんがお決まりの書き方のようなので書いておきます。
# app/controllers/application_controller.rb include Garage::ControllerHelper def current_resource_owner @current_resource_owner ||= User.find(resource_owner_id) if resource_owner_id end
usersコントローラとpostsコントローラを作成します。
bundle exec rails g controller users bundle exec rails g controller posts
コントローラにGarage::RestfulActions
をincludeすることで、index/create/show/update/deleteメソッドそれぞれをラップしたrequire_resources/create_resource/require_resource/update_resource/destroy_resourceメソッドが使えるようになります。
# app/controllers/users_controller.rb include Garage::RestfulActions # index def require_resources @resources = User.all end # show def require_resource @resource = User.find(params[:id]) end # update def update_resource @resource.update_attributes!(user_params) end private def user_params params.permit(:name) end
# app/controllers/posts_controller.rb include Garage::RestfulActions # index def require_resources if params[:user_id] @resources = User.find(params[:user_id]).posts else @resources = Post.all end end # create def create_resource @resources.create(post_params.merge(user_id: resource_owner_id)) end # show def require_resource @resource = Post.find(params[:id]) end # update def update_resource @resource.update_attributes!(post_params) end # destroy def destroy_resource @resource.destroy! end private def post_params params.permit(:title, :body, :published_at) end
ルーティングを設定します。postリソースはuserリソースにネストされるようにし、必要なアクションだけを設定しておきます。
# config/routes.rb scope :v1 do resources :users, only: %i(index show update) do resources :posts, shallow: true, except: %i(new edit) end end
モデルとリソースの定義
userモデルとpostモデルを作成し、マイグレーションします。
bundle exec rails g model user name:string email:string bundle exec rails g model post title:string body:string published_at:datetime user:references bundle exec rake db:migrate
モデルでレスポンスの内容を定義します。property
は属性、link
はリンク、collection
はアソシエーションしたモデルの情報を返します。selectable
オプションをtrueにすると、デフォルトでは値が返らず、リクエストで何らかのパラメータを与えることで返るようになるのだと思いますがパラメータの与え方が分かりませんでした。
# app/model/user.rb include Garage::Representer has_many :posts property :id property :name property :email link(:posts) { user_posts_path(self) } collection :posts
# app/model/post.rb include Garage::Representer belongs_to :user property :id property :title property :body property :published_at property :user, selectable: true
ローカルサーバーでリクエストを試す
テストデータを準備します。
bundle exec rails c user = User.create(name: "name1", email: "mail1@example.com") user.posts.create(title: 'title1', body: 'body1', published_at: DateTime.now) user.posts.create(title: 'title2', body: 'body2', published_at: DateTime.now) user.posts.create(title: 'title3', body: 'body3', published_at: DateTime.now)
サーバを起動します。
bundle exec rails s
ターミナルからcurlで/v1/users/:id
にGETリクエストするとjsonデータが返されます。モデルに設定したproperty
、link
、collection
、それぞれが表示されているのが分かりますでしょうか。
curl -s http://localhost:3000/v1/users/1 | jq . { "id": 1, "name": "name1", "email": "mail1@example.com", "_links": { "posts": { "href": "/v1/users/1/posts" } }, "posts": [ { "id": 1, "title": "title1", "body": "body1", "published_at": "2015-09-23T15:23:53.828Z" }, { "id": 2, "title": "title2", "body": "body2", "published_at": "2015-09-23T15:24:18.783Z" }, { "id": 3, "title": "title3", "body": "body3", "published_at": "2015-09-23T15:24:25.218Z" } ] }
所感
今回はgarageのAPI機能とハイパーメディア要素の作り方の一部を紹介しました。ご覧頂いた通り、簡単にハイパーメディアなAPIを作成することができました。クックパッドさんのサンプルコードを見たところ、他にもページネーションの要素を出力する方法などがありそうでしたが、それ以上の情報がなく試すところまでは至りませんでした。サンプルコードの時点からも結構バージョンアップしていて書き方とかも変わっていて、今まさに開発中という感じでしたのでこれからに期待しましょう!また、今回は省略した認証やアクセスコントロールは別の機会に紹介したいと思います。